<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h2 data-lake-id="3c4e0a25" id="3c4e0a25"><span data-lake-id="ucacfe6c9" id="ucacfe6c9">典型回答<br><br></span></h2>
  <p data-lake-id="u2a17b572" id="u2a17b572"><br></p>
  <p data-lake-id="uf8edaecd" id="uf8edaecd"><span data-lake-id="ueb2fa1e7" id="ueb2fa1e7">面试官真的就只是想听一下什么是单元测试么？如果真的这么想是大错特错。问出这个问题的面试官考察面试者以下3点：</span></p>
  <ol list="u61e3799f">
   <li fid="u252b29c7" data-lake-id="uaf3531a9" id="uaf3531a9"><span data-lake-id="u8357eb23" id="u8357eb23">对软件工程的生命周期是否熟悉，是否充分理解测试阶段各种手段（单测、集成测试、冒烟测试等）和重要性。</span></li>
   <li fid="u252b29c7" data-lake-id="u787b4a54" id="u787b4a54"><span data-lake-id="u7ce47fa0" id="u7ce47fa0">面试者是否有足够的责任心，做好测试工作是对自己代码充分负责。</span></li>
   <li fid="u252b29c7" data-lake-id="ucdb30fbd" id="ucdb30fbd"><span data-lake-id="u3328bb91" id="u3328bb91">优秀的单测用例也体现出开发者的设计和编码的基本素质。</span></li>
  </ol>
  <p data-lake-id="u4b748f6b" id="u4b748f6b"><br></p>
  <p data-lake-id="u0c6c7c61" id="u0c6c7c61"><span data-lake-id="u1c0d8dfe" id="u1c0d8dfe">基于以上3点，我们需要思考什么是有效的单元测试？</span></p>
  <p data-lake-id="u39beb237" id="u39beb237"><br></p>
  <blockquote data-lake-id="ue14b64c5" id="ue14b64c5">
   <p data-lake-id="u8939e6e9" id="u8939e6e9"><span data-lake-id="uc20a8642" id="uc20a8642">题外话，《有效的单元测试》是一本非常值得推荐看的书</span></p>
  </blockquote>
  <p data-lake-id="u4d624a61" id="u4d624a61"><br></p>
  <p data-lake-id="u72214562" id="u72214562"><span data-lake-id="u3597ad57" id="u3597ad57">整个软件工程的生命周期，大致为：</span></p>
  <p data-lake-id="u11bd823d" id="u11bd823d"><br></p>
  <ul list="u5154dcf7">
   <li fid="u170054a4" data-lake-id="u81aeb702" id="u81aeb702"><span data-lake-id="u3142cf84" id="u3142cf84">需求分析阶段：需求调研、设计、评审</span></li>
   <li fid="u170054a4" data-lake-id="ud90c6881" id="ud90c6881"><span data-lake-id="u30dccf42" id="u30dccf42">设计阶段：这里指架构设计阶段</span></li>
   <li fid="u170054a4" data-lake-id="u8e9205c9" id="u8e9205c9"><span data-lake-id="u283eb1d2" id="u283eb1d2">开发阶段：开始正式编码</span></li>
   <li fid="u170054a4" data-lake-id="u0b387bf3" id="u0b387bf3"><span data-lake-id="u7d9c6ba6" id="u7d9c6ba6">测试阶段：完成编码，包括： </span></li>
  </ul>
  <ul list="u5154dcf7" data-lake-indent="1">
   <li fid="u89605c96" data-lake-id="u718f6929" id="u718f6929"><span data-lake-id="u129006be" id="u129006be">自测：单元测试-&gt;集成测试</span></li>
   <li fid="u89605c96" data-lake-id="u986f6dd9" id="u986f6dd9"><span data-lake-id="u077d6824" id="u077d6824">提测：QA介入集成测试，多轮测试</span></li>
  </ul>
  <ul list="u5154dcf7" start="5">
   <li fid="u170054a4" data-lake-id="u0c0abf69" id="u0c0abf69"><span data-lake-id="ua970f075" id="ua970f075">发布：QA完成测试，可以进行上线，包括： </span></li>
  </ul>
  <ul list="u5154dcf7" data-lake-indent="1">
   <li fid="u33f3ce1b" data-lake-id="ubc948424" id="ubc948424"><span data-lake-id="u894d4467" id="u894d4467">预发布：部署到线上环境，QA进行回归测试，没问题放一部分流量进来，观察是否存在异常</span></li>
   <li fid="u33f3ce1b" data-lake-id="u4b907a93" id="u4b907a93"><span data-lake-id="uf3afdf27" id="uf3afdf27">上线：若预发布没问题，则代码正式上线，当然也会根据各种灰度或ab实验策略控制新功能流量占比，稳定跑一段时间没问题再放开全部流量</span></li>
  </ul>
  <p data-lake-id="u3712b1a8" id="u3712b1a8"><br></p>
  <p data-lake-id="u4dd8b84f" id="u4dd8b84f"><span data-lake-id="u9373b83f" id="u9373b83f">我们再来仔细分析每个阶段发现bug的成本，该成本主要是指从发现到解决问题的人力时间成本。</span></p>
  <p data-lake-id="uf4d932dd" id="uf4d932dd"><br></p>
  <ul list="u0f1148d4">
   <li fid="u7b318aa2" data-lake-id="u4f032f9c" id="u4f032f9c"><span data-lake-id="u1c860f5f" id="u1c860f5f">需求分析阶段：若大家评审设计不合理，可以不做，只占用小时级别的会议时间</span></li>
   <li fid="u7b318aa2" data-lake-id="u2024a8b8" id="u2024a8b8"><span data-lake-id="u5b17c843" id="u5b17c843">设计阶段：架构设计也需要进行评审，同样只占用小时级别的会议时间</span></li>
   <li fid="u7b318aa2" data-lake-id="u4e3b7f09" id="u4e3b7f09"><span data-lake-id="ude7f9ace" id="ude7f9ace">开发阶段：若前两个阶段没有发现问题，通常小功能是小时级别，大功能是天级别（甚至是星期、月），可能导致开发出无效的功能，重新做设计，重新开发的返工局面</span></li>
   <li fid="u7b318aa2" data-lake-id="ud0d0e02e" id="ud0d0e02e"><span data-lake-id="u438d6fcb" id="u438d6fcb">测试阶段：无论是自测还是提测的集成测试，修改一个bug就意味着需要重新部署代码，大型项目启动时间可能是分钟级别。无论是自测还是提测，意味着会阻塞当前测试进度，bug多了反复部署累计出来的时间会非常高。而单元测试一个case通常是毫秒或秒级别的，做好单元测试可大幅度提升效率。很多公司非常注重单测覆盖率，有效性，甚至把单测放入CI/CD中，所有单测都跑成功才能部署。同时QA也会非常注重阻塞测试进度的情况。</span></li>
   <li fid="u7b318aa2" data-lake-id="u80e2517a" id="u80e2517a"><span data-lake-id="uc20fe247" id="uc20fe247">发布阶段：通常进入发布阶段都是经过QA严格测试过的代码，不会出现明显的bug，但不代表没有。有一些bug可能在真实用户请求或流量大的时候才能出现，bug逃过了测试和预发布环境的测试，但到线上会直接出现。灰度和ab实验的一部分目标也是为了将线上的问题产生的影响降低到最小。这也是为啥各大互联网公司那么多大佬，依然会出现事故。这时候不只是时间成本，一个致命的bug可能带来直接的经济损失和用户流失。程序员一旦出了事故，就是个故事了。所以很多公司非常注重bug逃逸率，即在测试阶段没测出来的问题。</span></li>
  </ul>
  <p data-lake-id="uf7f86103" id="uf7f86103"><br></p>
  <p data-lake-id="u73340928" id="u73340928"><strong><span data-lake-id="u75b5cdbe" id="u75b5cdbe">上面说了这么多，也是为了突出单元测试的重要性</span></strong><span data-lake-id="u047f8c02" id="u047f8c02">。以下总结一下写好单测的方法：</span></p>
  <p data-lake-id="ue1038009" id="ue1038009"><br></p>
  <ol list="uc5046a52">
   <li fid="uab57644d" data-lake-id="ub70ee31c" id="ub70ee31c"><span data-lake-id="u83cb0135" id="u83cb0135"> </span><strong><span data-lake-id="u96fdcc52" id="u96fdcc52">单测代码与正式代码一样重要，所以要层次清晰</span></strong><span data-lake-id="u98e97f2a" id="u98e97f2a">，命名符合实际用例场景，要有适当的注释，《代码整洁之道》的技巧同样可用于单测代码中。很重要的一点：case要让人能看懂 </span></li>
   <li fid="uab57644d" data-lake-id="uae28695d" id="uae28695d"><span data-lake-id="u8b026f78" id="u8b026f78"> </span><strong><span data-lake-id="uc65d7653" id="uc65d7653">单测不要盲目追求覆盖率，但要尽量测出所有可能的场景 </span></strong></li>
   <li fid="uab57644d" data-lake-id="u0880ded5" id="u0880ded5"><span data-lake-id="ubc3d4f54" id="ubc3d4f54"> </span><strong><span data-lake-id="u3ad2f921" id="u3ad2f921">单测要保持可用</span></strong><span data-lake-id="u67e52d58" id="u67e52d58">，纳入CI/CD的流程，如果所有case跑不通，不允许部署 </span></li>
   <li fid="uab57644d" data-lake-id="u25d53e66" id="u25d53e66"><span data-lake-id="u964c921b" id="u964c921b"> 确保case每次运行是确定的，不依赖外部变化和不确定因素，包括不限于： </span></li>
  </ol>
  <ul list="u7fbbfdad" data-lake-indent="1">
   <li fid="u41402fcf" data-lake-id="uc01e95b7" id="uc01e95b7"><span data-lake-id="uf0fc067c" id="uf0fc067c">随机事件：如随机数，如果有最好Mock掉</span></li>
   <li fid="u41402fcf" data-lake-id="uac235da9" id="uac235da9"><span data-lake-id="ue266b56a" id="ue266b56a">IO：无论是磁盘IO、网络IO（数据库、外部接口）都需要隔离掉，否则IO抖一抖case就会失败，如果有最好Mock掉</span></li>
  </ul>
  <ol list="u00c4925d" start="5">
   <li fid="u7ccb20ba" data-lake-id="ua2ce17d2" id="ua2ce17d2"><span data-lake-id="u5cfb6e8c" id="u5cfb6e8c"> </span><strong><span data-lake-id="u1fc582e5" id="u1fc582e5">必须有断言，否则单测没有意义</span></strong><span data-lake-id="u58b80369" id="u58b80369">，不要想着打印一下结果肉眼看一下就可以了，在跑全部case的时候谁会经常想着要看一眼？ </span></li>
   <li fid="u7ccb20ba" data-lake-id="u5504e8dc" id="u5504e8dc"><span data-lake-id="uc0787fbe" id="uc0787fbe"> 验证边界和异常，这俩个是最容易被忽略的。边界可包括： </span></li>
  </ol>
  <p data-lake-id="uc8b0df3d" id="uc8b0df3d"><br></p>
  <ul list="u6f00084c">
   <li fid="u259c728f" data-lake-id="u10362e04" id="u10362e04"><span data-lake-id="ueb66ba74" id="ueb66ba74">传入错误参数会怎么样</span></li>
   <li fid="u259c728f" data-lake-id="u5ae0229a" id="u5ae0229a"><span data-lake-id="u3dafa467" id="u3dafa467">依赖（内部或外部接口、数据库环境等）返回不正确的结果会怎么样</span></li>
  </ul>
  <p data-lake-id="ub8a6d5ea" id="ub8a6d5ea"><br></p>
  <p data-lake-id="u1771a267" id="u1771a267"><span data-lake-id="u6dee1186" id="u6dee1186">异常包括：</span></p>
  <p data-lake-id="u5cd80906" id="u5cd80906"><br></p>
  <ul list="uaaab00bf">
   <li fid="uafa42daf" data-lake-id="u633f456d" id="u633f456d"><span data-lake-id="ub208b3a1" id="ub208b3a1">外部异常：依赖（内部或外部接口、数据库环境等）向调用者抛出异常会怎么样</span></li>
   <li fid="uafa42daf" data-lake-id="u44b8f11a" id="u44b8f11a"><span data-lake-id="u51f23f1c" id="u51f23f1c">内部异常：代码自身抛出</span><code data-lake-id="u29b872ca" id="u29b872ca"><span data-lake-id="u26e52681" id="u26e52681">RuntimeException</span></code><span data-lake-id="u695d7c82" id="u695d7c82">会怎么样</span></li>
  </ul>
  <p data-lake-id="u8a562378" id="u8a562378"><br></p>
  <ol list="u509b9407" start="7">
   <li fid="u3fe1f9f5" data-lake-id="u9e90b596" id="u9e90b596"><span data-lake-id="u1f58bf00" id="u1f58bf00">正式业务代码保证单一职责，高内聚低耦合可让单测更轻松，测试粒度更细，覆盖率也更高。一个方法一个类只干一个事，单测case就可以只关注当前要测试方法的有效性就行了，不需要关注方法之间的调用，如果你的每个方法都测通了，组合起来也都会通的。当然一个case也要只干一件事！</span></li>
  </ol>
  <p data-lake-id="u83fbc064" id="u83fbc064"><br></p>
  <p data-lake-id="ua28be39f" id="ua28be39f"><span data-lake-id="u0c7def9a" id="u0c7def9a">还有一个好的方法是采用TDD（测试驱动开发），即先列出所有可能的用例，再实现逻辑代码。这样产出的代码可快速构建出单测。不过国内很少有采用TDD开发模式的公司。</span></p>
  <p data-lake-id="ud29ced0b" id="ud29ced0b"><br></p>
  <blockquote data-lake-id="ufa270878" id="ufa270878">
   <p data-lake-id="uabf2fd76" id="uabf2fd76"><span data-lake-id="ue09fb31e" id="ue09fb31e">题外话，DDD（领域驱动设计）中提倡明确的边界划分，事件风暴，防腐层的设计都给TDD和单元测试做了很好的铺垫。</span></p>
  </blockquote>
  <p data-lake-id="u701431ef" id="u701431ef"><br></p>
  <p data-lake-id="u0c280de4" id="u0c280de4"><span data-lake-id="ub36400eb" id="ub36400eb">上面说使用Mock屏蔽IO和随机事件，当然也可以用在各种依赖上（如spring bean之间的依赖，工具类、各种内部接口的依赖），Mock是将所依赖的资源造假，我们假装依赖都是成功或失败的，这样只要测试自身代码对其产生了什么结果。<br><br></span></p>
  <p data-lake-id="uffa769d0" id="uffa769d0"><span data-lake-id="u6823b81a" id="u6823b81a"><br>
    Java工程也可以集成</span><code data-lake-id="u76cbc51a" id="u76cbc51a"><span data-lake-id="u7e85f9d9" id="u7e85f9d9">spock framework</span></code><span data-lake-id="u3c1152f7" id="u3c1152f7">进行单测，spock使用</span><code data-lake-id="u14c1147a" id="u14c1147a"><span data-lake-id="udc23f6be" id="udc23f6be">groovy</span></code><span data-lake-id="ub6262221" id="ub6262221">语言编写单测。由于是动态语言非常灵活，很适合编写轻量的单测代码。同时spock不只局限于Mock，还提供各种高效的功能（这些都是传统Junit、Mockito无法实现的）：</span></p>
  <p data-lake-id="ue2a0e7ef" id="ue2a0e7ef"><br></p>
  <ol list="udd683564">
   <li fid="u80ec09ac" data-lake-id="ucfbc5336" id="ucfbc5336"><span data-lake-id="u191023dd" id="u191023dd">Spy，只对部分资源进行Mock，可以很方便对同一个类内相互调用的方法进行Mock和验证</span></li>
   <li fid="u80ec09ac" data-lake-id="u879e5d6e" id="u879e5d6e"><span data-lake-id="u13ab5d46" id="u13ab5d46">Mock，对依赖资源进行Mock，同时验证依赖的资源调用多少次，比如：测试一个Redis写功能，可以对Redis客户端进行Mock，验证传入的方法参数是否符合预期，验证Redis写入方法的调用次数</span></li>
   <li fid="u80ec09ac" data-lake-id="u192380cc" id="u192380cc"><span data-lake-id="ue8ec1b59" id="ue8ec1b59">Stub，对依赖资源进行Mock一个结果，不关注调用次数、传参是否符合预期</span></li>
   <li fid="u80ec09ac" data-lake-id="uba962562" id="uba962562"><span data-lake-id="ub9320414" id="ub9320414">直接无视待验证方法的成员封装等级，可以直接测试</span><code data-lake-id="uc70629c5" id="uc70629c5"><span data-lake-id="u312e521b" id="u312e521b">private</span></code><span data-lake-id="ub4884a99" id="ub4884a99">声明的方法和变量</span></li>
   <li fid="u80ec09ac" data-lake-id="u17135d04" id="u17135d04"><span data-lake-id="u47434b22" id="u47434b22">基于数据驱动的测试，可以在一个case里通过</span><code data-lake-id="u1b2d77e8" id="u1b2d77e8"><span data-lake-id="uebd5fd1e" id="uebd5fd1e">where:</span></code><span data-lake-id="udfa9c6f9" id="udfa9c6f9">关键词和数据表格的方式对要测试的参数预期返回值的所有可能情况进行验证</span></li>
   <li fid="u80ec09ac" data-lake-id="u9abb291c" id="u9abb291c"><span data-lake-id="u0e09c8d4" id="u0e09c8d4">可以很方便的验证要抛出的异常</span></li>
   <li fid="u80ec09ac" data-lake-id="u2364ecdd" id="u2364ecdd"><span data-lake-id="u0a732222" id="u0a732222">可以很方便的集成spring，包括可单测spring mvc、spring boot的http接口层，不需要启动web容器</span></li>
  </ol>
  <p data-lake-id="u6f547b24" id="u6f547b24"><br></p>
  <p data-lake-id="u86446850" id="u86446850"><span data-lake-id="u0bf88ab4" id="u0bf88ab4">以上是如何做好单测的思路，请记住：</span></p>
  <p data-lake-id="ud4e6650d" id="ud4e6650d"><br></p>
  <p data-lake-id="ua8beac14" id="ua8beac14"><strong><span data-lake-id="ubf9ace5e" id="ubf9ace5e">1. 有用户访问的项目和无用户访问的项目是不一样的，相同的代码甚至在极端的用户流量下会产生不一样的效果。对于极端的用户流量，改一行代码上线都是如履薄冰</span></strong><span data-lake-id="u79671b7c" id="u79671b7c"><br></span><strong><span data-lake-id="u83ae3848" id="u83ae3848">2. 敬畏每一次上线和线上操作</span></strong></p>
  <p data-lake-id="u36e6f33d" id="u36e6f33d"><br></p>
  <p data-lake-id="uc336beaf" id="uc336beaf"><span data-lake-id="u7ff77dbe" id="u7ff77dbe">所以能够写出好的单测代码，是一个优秀程序员的基本素养。</span></p>
 </body>
</html>